package xyz.scootaloo.mono.security.service

import cn.hutool.json.JSONUtil
import cn.hutool.log.LogFactory
import org.apache.shiro.SecurityUtils
import org.apache.shiro.authc.AuthenticationException
import org.apache.shiro.authc.UsernamePasswordToken
import org.apache.shiro.subject.Subject
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.ApplicationArguments
import org.springframework.boot.ApplicationRunner
import org.springframework.data.redis.core.StringRedisTemplate
import org.springframework.stereotype.Component
import xyz.scootaloo.mono.base.lang.Call
import xyz.scootaloo.mono.base.lang.CallingResult
import xyz.scootaloo.mono.base.lang.SC
import xyz.scootaloo.mono.base.util.EncryptUtil
import xyz.scootaloo.mono.base.util.IpUtil
import xyz.scootaloo.mono.data.cache.MenuCacheManager
import xyz.scootaloo.mono.data.cache.RoleCacheManager
import xyz.scootaloo.mono.data.entity.auth.AuthUser
import xyz.scootaloo.mono.data.service.AuthService
import xyz.scootaloo.mono.security.realm.PasswordRealm
import xyz.scootaloo.mono.security.token.ClientID
import xyz.scootaloo.mono.security.util.*
import xyz.scootaloo.mono.security.web.dto.LoginFormVO
import java.util.concurrent.TimeUnit
import javax.servlet.ServletResponse
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse

/**
 * 提供用户注册登陆操作所需要的功能
 *
 * - 调用 shiro 完成 登陆, 登出
 * - 用户注册
 * - 签发 jwt
 * - todo 生成验证码图片, 验证用户提交的验证码
 *
 * @author flutterdash@qq.com
 * @since 2021/7/26 21:21
 */
@Component
class AccountService : ApplicationRunner {
    private val log = LogFactory.get()

    @Autowired private lateinit var redis: StringRedisTemplate
    @Autowired private lateinit var authService: AuthService

    override fun run(args: ApplicationArguments?) {
        val roles = authService.listAuthRoles()
        RoleCacheManager.refresh(roles)    // 1
        val menus = authService.listAuthMenus()
        MenuCacheManager.refresh(menus)    // 2
        log.info("security模块加载完毕, 加载数据库角色${roles.size}个, 菜单${menus.size}个.")
    }

    /**
     * 返回当前用户的 [ClientID] 对象.
     *
     * 假如当前用户未登陆, 否则抛出 [NullPointerException]
     *
     * @return 当前登陆用户的 [ClientID] 对象
     */
    fun getCurrentClient(): ClientID = getSubject().principals!!.primaryPrincipal!! as ClientID

    /**
     * 登陆一个用户
     *
     * 尝试登陆一个用户, 这里会检查用户的账号密码是否和数据库中的记录一致, 假如确认无误,
     * 则登陆成功, 方法返回一个 [CallingResult] 对象
     *
     * @param loginForm 要执行登陆操作的用户表单
     * @param request  用户客户端发起的请求, 此方法需要从请求中读取客户端信息
     * @param response 返回给前端的http响应, 此方向需要向响应中写入token信息
     * @return         假如登陆成功, 则调用`callingResult.success`返回true, 否则登陆失败
     *                 假如登陆失败, 可调用`callingResult.exception`分析异常
     */
    fun login(
        loginForm: LoginFormVO,
        request: HttpServletRequest,
        response: HttpServletResponse
    ): Call<Void> {
        val subject = getSubject()
        return try {
            val token = UsernamePasswordToken(loginForm.username, loginForm.password)
            /** see [PasswordRealm.doGetAuthenticationInfo] */
            subject.login(token)

            val client = subject.principal as ClientID
            val newToken = signNewJWT(request, client)
            response.setHeader(KW_HTTP.JWT_MARK, newToken)

            log.info("用户`${loginForm.username}`登陆成功")
            authService.updateUserLastLogin(client.uid)
            Call.success()
        } catch (authcException: AuthenticationException) {
            log.error("用户`${loginForm.username}`登陆失败, 错误信息: ${authcException.message}")
            Call.fail()
        } catch (exception: Exception) {
            log.error("执行登陆时遇到异常", exception)
            response.status = KW_STATUS.INTERNAL_SERVER_ERROR
            Call.fail(exception)
        }
    }

    /**
     * 登出用户
     */
    fun logout() {
        val subject = getSubject()
        if (subject.principals != null) { // 假如用户已登陆, 则清一下缓存
            val client = subject.principals.primaryPrincipal as ClientID
            doUserLogout(client)
        }
        subject.logout()
    }

    /**
     * 给[loginForm]执行注册操作
     *
     * 注册流程包括:
     * - 检查表单内容的合法性,完整性
     * - 检查是否满足存入数据库的要求
     * - 将用户数据存入数据库
     *
     * @param request  注册操作需要从请求报文中读取一些信息
     * @param loginForm 执行注册操作的用户表单
     * @return         注册操作的执行情况, [CallingResult.success]代表是否注册成功,
     *   假如注册不成功, [CallingResult.result]内含有注册失败的状态码
     */
    fun register(request: HttpServletRequest, loginForm: LoginFormVO): Call<SC> {
        val completenessCheck = isFormComplete(loginForm)
        if (!completenessCheck.success)
            return completenessCheck
        return doRegister(request, loginForm)
    }

    /**
     * 给用户签发新的 jwt
     *
     * 这个方法会随机生成一个 salt, 并把 salt 的副本存入 redis 缓存中,
     * redis中的 salt 副本指定的过期时间是jwt过期时间的两倍
     *
     * 当redis中缓存失效, 代表用户需要重新登陆;
     * 当jwt过期而redis缓存没有失效, 则系统会帮用户刷新jwt;
     *
     * @param request  用户客户端提交的请求
     * @param client   用户的权限信息
     */
    fun signNewJWT(request: HttpServletRequest, client: ClientID): String {
        val salt = EncryptUtil.generateSalt()
        client.updateAppId()
        client.updateIpAddress(request)
        redis.opsForValue().set(
            wrap(client.username, client.ip, client.appId), salt,
            KW_REDIS.TOKEN_REFRESH_INTERVAL * 2, TimeUnit.SECONDS
        )
        return JwtHelper.sign(client, salt)
    }

    /**
     * 根据 jwt 来验证一个用户的身份
     *
     * 由于签发此jwt的时候, 使用了一个密匙用于加密, 这个密匙存放在redis中,
     * 所以这里根据这个redis缓存, 来判断用户是否需要重新登陆. (缓存过期, 则需要重新登陆)
     *
     * 由于特殊情况, jwt字符串可能是被修改过的, 从而导致解析jwt的时候失败,
     * 这种情况也需要重新登陆, 以获取新的jwt
     *
     * 假如处理正常, 则这里会提取出jwt中的信息, 创建一个[ClientID]对象,
     * 对象中包含用户的标识以及权限等信息
     *
     * @param request 用户客户端发起的请求
     * @param token   request-header上的token属性, 是一个jwt字符串
     */
    fun verifyUserFromJWT(request: HttpServletRequest, token: String): Call<ClientID> {
        try {
            val ip = IpUtil.getRealIpAddress(request)
            val tokenHolder = JwtHelper.TokenHolder(token)
            val username = tokenHolder.username
            val appId = tokenHolder.appId
            val secret = redis.opsForValue().get(wrap(username, ip, appId))
            if (secret == null) { // redis缓存已失效
                log.info("用户`$username`持有的jwt已经过期, 需要重新登陆")
                return Call.fail()
            }

            return try {
                val claimMap = JwtHelper.verifyToken(token, secret)
                Call.success(
                    ClientID(
                        uid = claimMap[KW.uid]!!.asInt(),
                        username = claimMap[KW.username]!!.asString(),
                        roleCode = claimMap[KW.roleCode]!!.asInt(),
                        appId = claimMap[KW.appId]!!.asString(),
                        ip = ip
                    )
                )
            } catch (verifyEx: Exception) {
                /** 假如这里抛出了异常, 则说明此jwt不是由本系统提供的[JwtHelper]中的方法签发的 */
                log.error("jwt格式正确, 但是jwt的密匙错误或者荷载内容缺失", verifyEx)
                Call.fail(verifyEx)
            }
        } catch (parserEx: Exception) {
            log.error("解析jwt失败, 格式也许有问题, `$token`", parserEx)
            return Call.fail(parserEx)
        }
    }

    /**
     * 向http-response写入json的响应体
     *
     * 这个方法是面向本模块内部使用的, 而且具有一定的危险
     *
     * @param response 准备返回给前端的一条响应
     */
    internal fun writeJsonBody(response: ServletResponse, any: Any) {
        try {
            /** 修改contentType无效, 解决方案 */
            /** https://blog.csdn.net/quan20111992/article/details/108167246 */
            response.contentType = KW_HTTP.JSON_CONTENT_TYPE
            response.writer.append(JSONUtil.toJsonStr(any))
        } catch (e: Exception) {
            log.error("向response写入内容时, 遇到问题", e)
        }
    }

    /**
     * 检查用户提交的表单是否完整
     * 完整: 指账号密码不为空, 且长度符合规范
     */
    private fun isFormComplete(loginForm: LoginFormVO): Call<SC> {
        if (loginForm.password.isBlank() || loginForm.username.isBlank())
            return Call.fail(SC.LACK_REGISTRATION_INFO)
        if (loginForm.password.length < 6)
            return Call.fail(SC.PASSWORD_IS_TOO_SHORT)
        return Call.success(SC.SUCCESS)
    }

    private fun doRegister(request: HttpServletRequest, loginForm: LoginFormVO): Call<SC> {
        val dbUser = authService.findAuthUserByName(loginForm.username)
        if (dbUser != null) // 数据库中已存在相同用户名的用户
            return Call.fail(SC.DUPLICATE_USERNAME)
        return try {
            val encoder = EncryptUtil.PasswordEncoder(loginForm.password)
            authService.addAuthUser(
                request,
                AuthUser.edit {
                    uid = 0
                    username = loginForm.username
                    password = encoder.encoded
                    this.salt = encoder.salt
                    roleCode = RoleCacheManager.defaultCode()
                }
            )
            log.info("用户`${loginForm.username}`注册成功")
            Call.success(SC.REGISTER_SUCCESS)
        } catch (e: Exception) {
            log.error("用户`${loginForm.username}`执行注册操作时遇到异常", e)
            Call(false, SC.SERVER_INTERNAL_ERROR, e)
        }
    }

    private fun doUserLogout(client: ClientID) {
        /*
            移除在用户登陆时产生的数据,
            删除用户的缓存, 记录日志等
         */
        log.info("用户`${client.username}`登出")
        redis.delete(wrap(client.username, client.ip, client.appId))
    }

    private fun getSubject(): Subject = SecurityUtils.getSubject()
    private fun wrap(key: String, ip: String, appId: String): String =
        "${KW_REDIS.TOKEN_PREFIX}:$key:$ip:$appId"
}
